/*
 * Copyright (C) 2011 Emmanuel Tourdot
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 *
 * $Id$
 */
package org.etourdot.xinclude.sax;

import com.google.common.base.Strings;
import org.antlr.runtime.RecognitionException;
import org.etourdot.xinclude.XIncProcAbstractFactory;
import org.etourdot.xinclude.XIncProcEngine;
import org.etourdot.xinclude.xpointer.XPointer;
import org.etourdot.xinclude.xpointer.XPointerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.*;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.NamespaceSupport;
import org.xml.sax.helpers.XMLFilterImpl;
import org.xml.sax.helpers.XMLReaderFactory;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Emmanuel Tourdot
 */
public class XIncludeFilter extends XMLFilterImpl
{
    private static final Logger LOG = LoggerFactory.getLogger(XIncludeFilter.class);
    
    private static final String XINCLUDE_NAMESPACE_URI = "http://www.w3.org/2001/XInclude";
    private static final String XINCLUDE_INCLUDE = "include";
    private static final String XINCLUDE_FALLBACK = "fallback";

    private final URI baseURI;
    private final Stack<URI> baseURIStack = new Stack<URI>();
    private final AtomicInteger level = new AtomicInteger(0);
    private final AtomicInteger baseLevel = new AtomicInteger(0);
    private final AtomicBoolean fallback = new AtomicBoolean(false);
    private final AtomicBoolean include = new AtomicBoolean(false);
    private final Map<String, String> namespaces = new HashMap<String, String>();
    private XIncProcAbstractFactory factory;

    public XIncludeFilter(final URI baseURI)
    {
        this.baseURI = baseURI;
        this.level.set(0);
        this.baseLevel.set(0);
        this.fallback.set(false);
        this.include.set(false);
    }

    public void setFactory(final XIncProcAbstractFactory factory)
    {
        this.factory = factory;
    }

    @Override
    public void startDocument() throws SAXException
    {
        LOG.trace("startDocument");
        if (this.baseLevel.getAndIncrement() == 0)
        {
            super.startDocument();
        }
    }

    @Override
    public void endDocument() throws SAXException
    {
        LOG.trace("endDocument");
        if (this.baseLevel.decrementAndGet() == 0)
        {
            super.endDocument();
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException
    {
        LOG.trace("endElement:{}", localName);
        this.level.decrementAndGet();
        if (XINCLUDE_NAMESPACE_URI.equals(uri))
        {
            if (XINCLUDE_INCLUDE.equals(localName))
            {
                if (fallback.get())
                {
                    throw new SAXException("No fallback element");
                }
                baseURIStack.pop();
                include.set(false);
            }
            if (XINCLUDE_FALLBACK.equals(localName))
            {
                fallback.set(false);
            }
        }
        else
        {
            super.endElement(uri, localName, qName);
        }
    }

    @Override
    public void startPrefixMapping(String prefix, String uri) throws SAXException
    {
        LOG.trace("startPrefixMapping:{}", prefix);
        if (!XINCLUDE_NAMESPACE_URI.equals(uri))
        {
            super.startPrefixMapping(prefix, uri);
        }
        namespaces.put(prefix, uri);
    }

    @Override
    public void endPrefixMapping(String prefix) throws SAXException
    {
        LOG.trace("endPrefixMapping:{}", prefix);
        super.endPrefixMapping(prefix);
        namespaces.remove(prefix);
    }

    @Override
    public void startElement(final String uri, final String localName, final String qName,
                             final Attributes atts) throws SAXException
    {
        LOG.trace("startElement:{}", localName);
        final AttributesImpl attsImpl = new AttributesImpl(atts);
        if (baseURIStack.size() > 0 && level.get() == baseLevel.get())
        {
            final URI base = baseURIStack.peek();
            attsImpl.addAttribute(NamespaceSupport.XMLNS, "base",
             "xml:base", "CDATA", base.toASCIIString());
        }
        this.level.incrementAndGet();
        if (XINCLUDE_NAMESPACE_URI.equals(uri))
        {
            if (XINCLUDE_INCLUDE.equals(localName))
            {
                include.set(true);
                final String href = atts.getValue("", "href");
                final String parseAtt = atts.getValue("", "parse");
                final String parse = parseAtt == null ? "xml" : parseAtt;
                final String xpointer = atts.getValue("", "xpointer");
                final String encoding = atts.getValue("", "encoding");
                final String accept = atts.getValue("", "accept");
                final String acceptLanguage = atts.getValue("", "accept-language");
                try
                {
                    checkAttributes(href, parse, xpointer, encoding, accept, acceptLanguage);
                    if ("xml".equals(parse))
                    {
                        includeXmlContent(href, xpointer, encoding, accept, acceptLanguage);
                    }
                    else
                    {
                        includeTextContent(href, encoding);
                    }
                }
                catch (final Exception e)
                {
                    fallback.set(true);
                }
                return;
            }
            if (XINCLUDE_FALLBACK.equals(localName))
            {
                if (!include.get())
                {
                    throw new SAXException("Fallback not in xinclude element container");
                }
                if (fallback.get())
                {
                    include.set(false);
                }
                return;
            }
        }
        super.startElement(uri, localName, qName, attsImpl);
    }

    private void includeXmlContent(final String href, final String xpointer, final String encoding,
                                   final String accept, final String acceptLanguage) throws SAXException
    {
        LOG.trace("includeXmlContent:{}", href);
        final URI source = getSource(href);
        try {
            if (!Strings.isNullOrEmpty(xpointer))
            {
                final XPointerContext xPointerContext = new XPointerContext(xpointer, factory.getConfiguration());
                for (Map.Entry<String, String> namespace : namespaces.entrySet())
                {
                    xPointerContext.addPrefix(namespace.getKey(), namespace.getValue());
                }
                XPointer pointer = factory.getConfiguration().getXPointerEngine().parse(xpointer);
                pointer.setContext(xPointerContext);
                //pointer.parse(source.toASCIIString());
                LOG.trace("parsing1 :{}", source.toASCIIString());
            }
            else
            {
                final XMLReader parser = XMLReaderFactory.createXMLReader();
                parser.setContentHandler(this);
                parser.setEntityResolver(new EntityResolver() {
                    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
                        return new InputSource(new ByteArrayInputStream("".getBytes()));
                    }
                });
                parser.parse(source.toASCIIString());
                LOG.trace("parsing2 :{}", source.toASCIIString());               
            }
        }
        catch (final IOException e)
        {
            throw new SAXException("Document not found : " + source.getPath());
        }
        catch (final RecognitionException e)
        {
            throw new SAXException("Recognition problem : " + source.getPath());
        }
    }

    private void includeTextContent(final String href, final String encoding) throws SAXException
    {
        LOG.trace("includeTextContent:{}", href);
        final URI source = getSource(href);

        final StringBuilder builder = new StringBuilder();
        try
        {
            final URL url = source.toURL();
            final URLConnection conn = url.openConnection();
            final String contentType = conn.getContentType();

            // Get the response
            final BufferedReader rd;
            if (encoding == null)
            {
                rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            }
            else
            {
                rd = new BufferedReader(new InputStreamReader(conn.getInputStream(), encoding));
            }

            String line;
            while ((line = rd.readLine()) != null)
            {
                builder.append(line).append("\n");
            }
            rd.close();
        }
        catch (final Exception e)
        {
            throw new SAXException(e);
        }
        final String content = builder.toString();
        super.characters(content.toCharArray(), 0, content.length());
    }

    private void checkAttributes(final String href, final String parse, final String xpointer,
                                 final String encoding, final String accept, final String acceptLanguage)
                                throws SAXException, URISyntaxException
    {
        if (Strings.isNullOrEmpty(href))
        {
            if ("xml".equals(parse) && Strings.isNullOrEmpty(xpointer))
            {
                throw new SAXException("If the href attribute is absent when parse=\"xml\", the xpointer attribute must be present.");
            }
        }
        else
        {
            if (href.contains("#"))
            {
                throw new SAXException("Fragment identifiers must not be used.");
            }
            baseURIStack.push(new URI(href));
        }
        if (Strings.isNullOrEmpty(parse))
        {
            if (!"xml".equals(parse) && !"text".equals(parse))
            {
                throw new SAXException("Parse value must be \"xml\" or \"text\".");
            }
            if ("text".equals(parse)) {
                if (!Strings.isNullOrEmpty(xpointer))
                {
                    throw new SAXException("The xpointer attribute must not be present when parse=\"text\"");
                }
                try
                {
                    Charset.forName(encoding);
                }
                catch (final Exception e)
                {
                    throw new SAXException("Encoding attribute should be a valid encoding name");
                }
            }
        }
        if ((!Strings.isNullOrEmpty(accept) && XIncProcEngine.checkVal(accept)) ||
            (!Strings.isNullOrEmpty(acceptLanguage) && XIncProcEngine.checkVal(acceptLanguage)))
        {
            throw new SAXException("Attribute containing characters outside the range #x20 through #x7E");
        }
    }

    private URI getSource(final String href) throws SAXException
    {
        try {
            URI source;
            if (href.contains(":"))
            {
                source = new URI(href);
            }
            else
            {
                source = getBase(baseURIStack.peek());
            }
            return source;
        } catch (URISyntaxException e) {
            throw new SAXException("href must be a syntactically invalid URI or IRI.");
        }
    }

    private URI getBase(final URI currentURI) throws SAXException
    {
        if (currentURI.isAbsolute())
        {
            return currentURI;
        }
        else
        {
            final URI newBase;
            if (baseURIStack.indexOf(currentURI) > 0)
            {
                newBase = baseURIStack.get(baseURIStack.indexOf(currentURI) - 1);
            }
            else
            {
                newBase = baseURI;
            }
            final URI resolvedUri = newBase.resolve(currentURI);
            if (resolvedUri.equals(currentURI))
            {
                throw new SAXException("Inclusion loop error");
            }
            return getBase(resolvedUri);
        }
    }

    public void characters(char[] ch, int start, int length) throws SAXException
    {
        LOG.trace("characters:{}", new String(ch));
        super.characters(ch, start, length);
    }
}
